package com.guyaogg.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guyaogg.common.utils.PageUtils;
import com.guyaogg.common.utils.Query;
import com.guyaogg.common.utils.StringUtils;
import com.guyaogg.gulimall.product.dao.CategoryDao;
import com.guyaogg.gulimall.product.entity.CategoryEntity;
import com.guyaogg.gulimall.product.service.CategoryBrandRelationService;
import com.guyaogg.gulimall.product.service.CategoryService;
import com.guyaogg.gulimall.product.vo.Catalog2VO;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    //    @Autowired
//    private CategoryDao categoryDao;
    private final StringRedisTemplate stringRedisTemplate;
    private final ReentrantLock lock = new ReentrantLock();
    private final String catalogJsonKey = "catalogJson";
    private final RedissonClient redisson;

    private final CategoryBrandRelationService categoryBrandRelationService;

    public CategoryServiceImpl(StringRedisTemplate stringRedisTemplate, CategoryBrandRelationService categoryBrandRelationService, RedissonClient redisson) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.categoryBrandRelationService = categoryBrandRelationService;
        this.redisson = redisson;
    }

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        // 2.组装成父子的树形结构
        // 2.1 找到所有一级分类
        List<CategoryEntity> level1Menus = getChildrens(0L, entities);


        return level1Menus;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        // TODO // 1.检查当前删除菜单，是否被引用

        // 逻辑删除

        baseMapper.deleteBatchIds(asList);

    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[0]);
    }

    /**
     * 级联更新所有关联数据
     * # @CacheEvict 的使用
     *
     * @param category 关联数据
     */
//    @CacheEvict(value = {"category"}, key = "'level1Categorys'")
//    @Caching(evict = {
//            @CacheEvict(value = {"category"}, key = "'level1Categorys'"),
//            @CacheEvict(value = {"category"}, key = "'getCatalogJson'")
//    })
    @SuppressWarnings("AlibabaCommentsMustBeJavadocFormat")
    @CacheEvict(value = {"category"},allEntries = true) // 删除分区内所有值
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateCascade(CategoryEntity category) {
        updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());

        }
    }

    /**
     * #@Cacheable 代表当前结果需要缓存，如果缓存中有，方法不用调用。没有就会调用方法，将结果放入缓存
     * // value = {"category","product"} 指定放入的缓存区名，建议按照【业务类型】分区
     * <p>
     * 默认行为 ：
     * key 默认自动生成，缓存名字 ，分区名字::SimpleKey []
     * value 默认使用 JDK 序列化机制，将序列化后数据存入 redis ，默认没有过期时间
     * <p>
     * 自定义：
     * 指定 key 名称  key = 指定 ，使用 el 表达式: https://docs.spring.io/spring-framework/docs/5.2.18.RELEASE/spring-framework-reference/integration.html#cache-annotations
     * 指定缓存存活时间，  在配置文件中修改
     * 使用自定义序列化机制 （ json
     * 自定义注入 RedisCacheConfiguration
     * // 统一业务类型的数据，都可以指定一个分区，很好批量删除
     *
     * @return
     */
    @Cacheable(value = {"category"}, key = "'level1Categorys'",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
        log.debug("花费时间：" + (System.currentTimeMillis() - l));
        return categoryEntities;
    }

    @Cacheable(value = {"category"}, key = "#root.methodName",sync = true)
    @Override
    public Map<String, List<Catalog2VO>> getCatalogJson() {
//        String catalogJson = stringRedisTemplate.opsForValue().get("CACHE_category::getCatalogJson");
//        if (!StringUtils.isEmpty(catalogJson)) {
//            System.out.println("缓存命中...redis");
//
//            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
//            });
//        }
         /*
        优化 1.将多次查询数据库变为 1 次
        优化 2.建立分布式缓存，整合 redis
         */
        log.info("缓存不命中...查询数据库");

        final List<CategoryEntity> selectList = baseMapper.selectList(null);

        // 查出所有一级分类
        List<CategoryEntity> level1Categorys = getParentCidList(selectList, 0L);
        if (!level1Categorys.isEmpty()) {
            // 2.封装数据
            Map<String, List<Catalog2VO>> catalogJsonFromDb = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                // 查到一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParentCidList(selectList, v.getCatId());
                List<Catalog2VO> catalog2VOList = new ArrayList<>();
                if (!categoryEntities.isEmpty()) {
                    catalog2VOList = categoryEntities.stream().map(l2 -> {
                        Catalog2VO catalog2VO = new Catalog2VO(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                        // 封装三级分类
                        List<CategoryEntity> entities3 = getParentCidList(selectList, l2.getCatId());
                        List<Catalog2VO.Catalog3VO> collect = null;
                        if (!entities3.isEmpty()) {
                            collect = entities3.stream().map(l3 -> new Catalog2VO.Catalog3VO(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName())).collect(Collectors.toList());

                        }
                        catalog2VO.setCatalog3List(collect);
                        return catalog2VO;
                    }).collect(Collectors.toList());

                }
                return catalog2VOList;
            }));
            return catalogJsonFromDb;
        }
        return null;

    }

    /**
     * TODO 堆外内存异常？？？（我没有，真奇怪
     * springboot2.0 之后默认使用 lettuce 作为客户端，使用 netty 进行网络通信 (2.3.2版本修复了？？
     *  lettuce 的 bug 异常，（好像修复了，我的没报错 ） netty 默认使用 Xmx 作为固定内存
     * <p>
     * 解决方案：调大 -Dio.netty.maxDirectMemory 只是延缓异常时间发生（还是会发生
     * 1 升级 lettuce 客户端
     * 2 直接切换使用 jedis ( 并发量不高
     */
    public Map<String, List<Catalog2VO>> getCatalogJson2() {
        // 获取锁,缓存不一致解决方法之一，加读写锁
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("catalogJson-rw-lock");
        RLock rLock = readWriteLock.readLock();
        // 获取锁
        rLock.lock();
        try {
            // 临界区
              /*
        解决 穿透、雪崩、击穿
        1.空结果缓存
        2.设置随机过期时间
        3.加锁
         */
            // 缓存使用，给缓存放入 json 字符串，取出使用 json 反序列化
            // 加入缓存逻辑
            String catalogJson = stringRedisTemplate.opsForValue().get(this.catalogJsonKey);
            if (StringUtils.isEmpty(catalogJson)) {
                // 缓存中没有 查询数据库,查询和放入缓存在同一操作

                // 使用分布式锁
                return getCatalogJsonFromDbWithRedisLock();

            }
            // 转为指定的对象
            log.info("缓存命中...redis");

            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });

        } finally {
            // 释放锁
            rLock.unlock();
        }


    }

    /**
     * 从数据查询并封装数据
     * <p>
     * 分布式锁，
     * <p>
     * 缓存里的数据如何和数据库保持一致
     * 1）双写模式 -> 脏数据问题 -> 1.加锁，2.对于一致性不高要求时，加过期时间
     * 2）失效模式 -> 脏数据问题 -> 1.加锁 2.经常修改的数据不要用缓存
     * 解决
     * 1.所有缓存加上过期时间
     * 2.分布式读写锁
     * 3.Canal
     *
     * @return
     */
    public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithRedisLock() {
//        // 1.占分布式锁，去 redis 占坑
//        String redisLock = "lock";
//        String redisLockKey = UUID.randomUUID().toString();
//        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(redisLock, redisLockKey, 300L, TimeUnit.SECONDS);

//        if (Boolean.TRUE.equals(lock)) {
        // 1.获取分布式锁,锁的名字决定了锁的力度
        String catalogRedisLockName = "catalogJson-lock";
        RLock lock = redisson.getLock(catalogRedisLockName);
        lock.lock(30L, TimeUnit.SECONDS);
        log.info("获取分布式锁成功");
        // 加锁成功
        // 设置过期时间,必须和加锁是原子操作
//            stringRedisTemplate.expire(redisLock,30L,TimeUnit.SECONDS);
        Map<String, List<Catalog2VO>> dataFromDb;
        try {
            // 执行业务
            dataFromDb = getDataFromDb();
        } finally {
//                // 取值和删除也要原子 ---> 使用 lua 脚本解锁
//                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//                // 原子取整和删锁，return 1 表示删除成功
//                Long isDeleteLock = stringRedisTemplate.execute(
//                        // 即使  Long.class
//                        new DefaultRedisScript<>(script,Long.class),
//                        Collections.singletonList(redisLock),
//                        redisLockKey);
//            String currentLockKey = stringRedisTemplate.opsForValue().get(redisLock);
//            // 删去自己的锁,,
//            if(redisLockKey.equals(currentLockKey)){
//                // 删去锁，给其他拿锁人机会,,,不能随便删，看uuid 是否一致
//                stringRedisTemplate.delete(redisLock);
//            }
            lock.unlock();
        }
        return dataFromDb;

    }
//        else {
//            System.out.println("获取分布式锁失败，等待重试...");
//            // 等待一下防止 cpu 过高
//            try {
//                Thread.sleep((long) ((Math.random() * 100)) + 1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            // 加锁失败，自旋方式获取锁
//            return this.getCatalogJsonFromDbWithRedisLock();
//        }


    private Map<String, List<Catalog2VO>> getDataFromDb() {
        String catalogJson = stringRedisTemplate.opsForValue().get(this.catalogJsonKey);
        if (!StringUtils.isEmpty(catalogJson)) {
            log.info("缓存命中...redis");

            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });
        }
         /*
        优化 1.将多次查询数据库变为 1 次
        优化 2.建立分布式缓存，整合 redis
         */
        log.info("缓存不命中...查询数据库");

        final List<CategoryEntity> selectList = baseMapper.selectList(null);

        // 查出所有一级分类
        List<CategoryEntity> level1Categorys = getParentCidList(selectList, 0L);
        if (!level1Categorys.isEmpty()) {
            // 2.封装数据
            Map<String, List<Catalog2VO>> catalogJsonFromDb = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                // 查到一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParentCidList(selectList, v.getCatId());
                List<Catalog2VO> catalog2VOList = new ArrayList<>();
                if (!categoryEntities.isEmpty()) {
                    catalog2VOList = categoryEntities.stream().map(l2 -> {
                        Catalog2VO catalog2VO = new Catalog2VO(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                        // 封装三级分类
                        List<CategoryEntity> entities3 = getParentCidList(selectList, l2.getCatId());
                        List<Catalog2VO.Catalog3VO> collect = null;
                        if (!entities3.isEmpty()) {
                            collect = entities3.stream().map(l3 -> new Catalog2VO.Catalog3VO(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName())).collect(Collectors.toList());

                        }
                        catalog2VO.setCatalog3List(collect);
                        return catalog2VO;
                    }).collect(Collectors.toList());

                }
                return catalog2VOList;
            }));
            // 查询后放入缓存,将数据转为 json 存入缓存中，快语言、跨平台兼容
            String jsonString = JSON.toJSONString(catalogJsonFromDb);
            // 雪崩和击穿
            stringRedisTemplate.opsForValue().set(this.catalogJsonKey, jsonString, (int) (Math.random() * 5) + 1, TimeUnit.DAYS);
            return catalogJsonFromDb;
        }
        // 穿透
        stringRedisTemplate.opsForValue().set(this.catalogJsonKey, "", (int) (Math.random() * 5) + 1, TimeUnit.HOURS);
        return null;

    }

    /**
     * 从数据查询并封装数据
     *
     * @return
     */
    public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithLocalLock() {
        // TODO 本地锁，只能锁住本地资源，想要锁住所有必须使用分布式锁
        // 获取锁
        lock.lock();
        try {
            // 临界区
            // 双检锁
            return getDataFromDb();
        } finally {
            // 释放锁
            lock.unlock();
        }


    }

    private List<CategoryEntity> getParentCidList(List<CategoryEntity> selectList, long parentCid) {
        return selectList.stream().filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        // 递归查找父 id 并收集
        paths.add(catelogId);
        CategoryEntity byId = getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), paths);
        }
        return paths;
    }

    /**
     * 递归查找所有菜单的子菜单
     *
     * @param rootId 父菜单的id
     * @param all    所有数据
     * @return 子菜单
     */
    private List<CategoryEntity> getChildrens(Long rootId, List<CategoryEntity> all) {
        List<CategoryEntity> children = all.stream().filter(categoryEntity -> Objects.equals(categoryEntity.getParentCid(), rootId)
                // 映射，递归查找子类
        ).peek(categoryEntity -> categoryEntity.setChildren(getChildrens(categoryEntity.getCatId(), all))
                // 排序
        ).sorted(Comparator.comparingInt((menu) -> menu.getSort() == null ? 0 : menu.getSort())
                // 归档
        ).collect(Collectors.toList());

        return children;
    }

}